/*!
 * The MIT License
 *
 * Copyright (c) 2012 James Allardice
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

(function(global) {

    'use strict';

    //
    // Test for support. We do this as early as possible to optimise for browsers
    // that have native support for the attribute.
    //

    var test = document.createElement('input');
    var nativeSupport = test.placeholder !== void 0;

    global.Placeholders = {
        nativeSupport: nativeSupport,
        disable: nativeSupport ? noop : disablePlaceholders,
        enable: nativeSupport ? noop : enablePlaceholders
    };

    if (nativeSupport) {
        return;
    }

    //
    // If we reach this point then the browser does not have native support for
    // the attribute.
    //

    // The list of input element types that support the placeholder attribute.
    var validTypes = [
        'text',
        'search',
        'url',
        'tel',
        'email',
        'password',
        'number',
        'textarea'
    ];

    // The list of keycodes that are not allowed when the polyfill is configured
    // to hide-on-input.
    var badKeys = [

        // The following keys all cause the caret to jump to the end of the input
        // value.

        27, // Escape
        33, // Page up
        34, // Page down
        35, // End
        36, // Home

        // Arrow keys allow you to move the caret manually, which should be
        // prevented when the placeholder is visible.

        37, // Left
        38, // Up
        39, // Right
        40, // Down

        // The following keys allow you to modify the placeholder text by removing
        // characters, which should be prevented when the placeholder is visible.

        8, // Backspace
        46 // Delete
    ];

    // Styling variables.
    var placeholderStyleColor = '#ccc';
    var placeholderClassName = 'placeholdersjs';
    var classNameRegExp = new RegExp('(?:^|\\s)' + placeholderClassName + '(?!\\S)');

    // The various data-* attributes used by the polyfill.
    var ATTR_CURRENT_VAL = 'data-placeholder-value';
    var ATTR_ACTIVE = 'data-placeholder-active';
    var ATTR_INPUT_TYPE = 'data-placeholder-type';
    var ATTR_FORM_HANDLED = 'data-placeholder-submit';
    var ATTR_EVENTS_BOUND = 'data-placeholder-bound';
    var ATTR_OPTION_FOCUS = 'data-placeholder-focus';
    var ATTR_OPTION_LIVE = 'data-placeholder-live';
    var ATTR_MAXLENGTH = 'data-placeholder-maxlength';

    // Various other variables used throughout the rest of the script.
    var UPDATE_INTERVAL = 100;
    var head = document.getElementsByTagName('head')[0];
    var root = document.documentElement;
    var Placeholders = global.Placeholders;
    var keydownVal;

    // Get references to all the input and textarea elements currently in the DOM
    // (live NodeList objects to we only need to do this once).
    var inputs = document.getElementsByTagName('input');
    var textareas = document.getElementsByTagName('textarea');

    // Get any settings declared as data-* attributes on the root element.
    // Currently the only options are whether to hide the placeholder on focus
    // or input and whether to auto-update.
    var hideOnInput = root.getAttribute(ATTR_OPTION_FOCUS) === 'false';
    var liveUpdates = root.getAttribute(ATTR_OPTION_LIVE) !== 'false';

    // Create style element for placeholder styles (instead of directly setting
    // style properties on elements - allows for better flexibility alongside
    // user-defined styles).
    var styleElem = document.createElement('style');
    styleElem.type = 'text/css';

    // Create style rules as text node.
    var styleRules = document.createTextNode(
        '.' + placeholderClassName + ' {' +
        'color:' + placeholderStyleColor + ';' +
        '}'
    );

    // Append style rules to newly created stylesheet.
    if (styleElem.styleSheet) {
        styleElem.styleSheet.cssText = styleRules.nodeValue;
    } else {
        styleElem.appendChild(styleRules);
    }

    // Prepend new style element to the head (before any existing stylesheets,
    // so user-defined rules take precedence).
    head.insertBefore(styleElem, head.firstChild);

    // Set up the placeholders.
    var placeholder;
    var elem;

    for (var i = 0, len = inputs.length + textareas.length; i < len; i++) {

        // Find the next element. If we've already done all the inputs we move on
        // to the textareas.
        elem = i < inputs.length ? inputs[i] : textareas[i - inputs.length];

        // Get the value of the placeholder attribute, if any. IE10 emulating IE7
        // fails with getAttribute, hence the use of the attributes node.
        placeholder = elem.attributes.placeholder;

        // If the element has a placeholder attribute we need to modify it.
        if (placeholder) {

            // IE returns an empty object instead of undefined if the attribute is
            // not present.
            placeholder = placeholder.nodeValue;

            // Only apply the polyfill if this element is of a type that supports
            // placeholders and has a placeholder attribute with a non-empty value.
            if (placeholder && inArray(validTypes, elem.type)) {
                newElement(elem);
            }
        }
    }

    // If enabled, the polyfill will repeatedly check for changed/added elements
    // and apply to those as well.
    var timer = setInterval(function() {
        for (var i = 0, len = inputs.length + textareas.length; i < len; i++) {
            elem = i < inputs.length ? inputs[i] : textareas[i - inputs.length];

            // Only apply the polyfill if this element is of a type that supports
            // placeholders, and has a placeholder attribute with a non-empty value.
            placeholder = elem.attributes.placeholder;

            if (placeholder) {

                placeholder = placeholder.nodeValue;

                if (placeholder && inArray(validTypes, elem.type)) {

                    // If the element hasn't had event handlers bound to it then add
                    // them.
                    if (!elem.getAttribute(ATTR_EVENTS_BOUND)) {
                        newElement(elem);
                    }

                    // If the placeholder value has changed or not been initialised yet
                    // we need to update the display.
                    if (
                        placeholder !== elem.getAttribute(ATTR_CURRENT_VAL) ||
                        (elem.type === 'password' && !elem.getAttribute(ATTR_INPUT_TYPE))
                    ) {

                        // Attempt to change the type of password inputs (fails in IE < 9).
                        if (
                            elem.type === 'password' &&
                            !elem.getAttribute(ATTR_INPUT_TYPE) &&
                            changeType(elem, 'text')
                        ) {
                            elem.setAttribute(ATTR_INPUT_TYPE, 'password');
                        }

                        // If the placeholder value has changed and the placeholder is
                        // currently on display we need to change it.
                        if (elem.value === elem.getAttribute(ATTR_CURRENT_VAL)) {
                            elem.value = placeholder;
                        }

                        // Keep a reference to the current placeholder value in case it
                        // changes via another script.
                        elem.setAttribute(ATTR_CURRENT_VAL, placeholder);
                    }
                }
            } else if (elem.getAttribute(ATTR_ACTIVE)) {
                hidePlaceholder(elem);
                elem.removeAttribute(ATTR_CURRENT_VAL);
            }
        }

        // If live updates are not enabled cancel the timer.
        if (!liveUpdates) {
            clearInterval(timer);
        }
    }, UPDATE_INTERVAL);

    // Disabling placeholders before unloading the page prevents flash of
    // unstyled placeholders on load if the page was refreshed.
    addEventListener(global, 'beforeunload', function() {
        Placeholders.disable();
    });

    //
    // Utility functions
    //

    // No-op (used in place of public methods when native support is detected).
    function noop() {}

    // Avoid IE9 activeElement of death when an iframe is used.
    //
    // More info:
    //  - http://bugs.jquery.com/ticket/13393
    //  - https://github.com/jquery/jquery/commit/85fc5878b3c6af73f42d61eedf73013e7faae408
    function safeActiveElement() {
        try {
            return document.activeElement;
        } catch (err) {}
    }

    // Check whether an item is in an array. We don't use Array.prototype.indexOf
    // so we don't clobber any existing polyfills. This is a really simple
    // alternative.
    function inArray(arr, item) {
        for (var i = 0, len = arr.length; i < len; i++) {
            if (arr[i] === item) {
                return true;
            }
        }
        return false;
    }

    // Cross-browser DOM event binding
    function addEventListener(elem, event, fn) {
        if (elem.addEventListener) {
            return elem.addEventListener(event, fn, false);
        }
        if (elem.attachEvent) {
            return elem.attachEvent('on' + event, fn);
        }
    }

    // Move the caret to the index position specified. Assumes that the element
    // has focus.
    function moveCaret(elem, index) {
        var range;
        if (elem.createTextRange) {
            range = elem.createTextRange();
            range.move('character', index);
            range.select();
        } else if (elem.selectionStart) {
            elem.focus();
            elem.setSelectionRange(index, index);
        }
    }

    // Attempt to change the type property of an input element.
    function changeType(elem, type) {
        try {
            elem.type = type;
            return true;
        } catch (e) {
            // You can't change input type in IE8 and below.
            return false;
        }
    }

    function handleElem(node, callback) {

        // Check if the passed in node is an input/textarea (in which case it can't
        // have any affected descendants).
        if (node && node.getAttribute(ATTR_CURRENT_VAL)) {
            callback(node);
        } else {

            // If an element was passed in, get all affected descendants. Otherwise,
            // get all affected elements in document.
            var handleInputs = node ? node.getElementsByTagName('input') : inputs;
            var handleTextareas = node ? node.getElementsByTagName('textarea') : textareas;

            var handleInputsLength = handleInputs ? handleInputs.length : 0;
            var handleTextareasLength = handleTextareas ? handleTextareas.length : 0;

            // Run the callback for each element.
            var len = handleInputsLength + handleTextareasLength;
            var elem;
            for (var i = 0; i < len; i++) {

                elem = i < handleInputsLength ?
                    handleInputs[i] :
                    handleTextareas[i - handleInputsLength];

                callback(elem);
            }
        }
    }

    // Return all affected elements to their normal state (remove placeholder
    // value if present).
    function disablePlaceholders(node) {
        handleElem(node, hidePlaceholder);
    }

    // Show the placeholder value on all appropriate elements.
    function enablePlaceholders(node) {
        handleElem(node, showPlaceholder);
    }

    // Hide the placeholder value on a single element. Returns true if the
    // placeholder was hidden and false if it was not (because it wasn't visible
    // in the first place).
    function hidePlaceholder(elem, keydownValue) {

        var valueChanged = !!keydownValue && elem.value !== keydownValue;
        var isPlaceholderValue = elem.value === elem.getAttribute(ATTR_CURRENT_VAL);

        if (
            (valueChanged || isPlaceholderValue) &&
            elem.getAttribute(ATTR_ACTIVE) === 'true'
        ) {

            elem.removeAttribute(ATTR_ACTIVE);
            elem.value = elem.value.replace(elem.getAttribute(ATTR_CURRENT_VAL), '');
            elem.className = elem.className.replace(classNameRegExp, '');

            // Restore the maxlength value. Old FF returns -1 if attribute not set.
            // See GH-56.
            var maxLength = elem.getAttribute(ATTR_MAXLENGTH);
            if (parseInt(maxLength, 10) >= 0) {
                elem.setAttribute('maxLength', maxLength);
                elem.removeAttribute(ATTR_MAXLENGTH);
            }

            // If the polyfill has changed the type of the element we need to change
            // it back.
            var type = elem.getAttribute(ATTR_INPUT_TYPE);
            if (type) {
                elem.type = type;
            }

            return true;
        }

        return false;
    }

    // Show the placeholder value on a single element. Returns true if the
    // placeholder was shown and false if it was not (because it was already
    // visible).
    function showPlaceholder(elem) {

        var val = elem.getAttribute(ATTR_CURRENT_VAL);

        if (elem.value === '' && val) {

            elem.setAttribute(ATTR_ACTIVE, 'true');
            elem.value = val;
            elem.className += ' ' + placeholderClassName;

            // Store and remove the maxlength value.
            var maxLength = elem.getAttribute(ATTR_MAXLENGTH);
            if (!maxLength) {
                elem.setAttribute(ATTR_MAXLENGTH, elem.maxLength);
                elem.removeAttribute('maxLength');
            }

            // If the type of element needs to change, change it (e.g. password
            // inputs).
            var type = elem.getAttribute(ATTR_INPUT_TYPE);
            if (type) {
                elem.type = 'text';
            } else if (elem.type === 'password' && changeType(elem, 'text')) {
                elem.setAttribute(ATTR_INPUT_TYPE, 'password');
            }

            return true;
        }

        return false;
    }

    // Returns a function that is used as a focus event handler.
    function makeFocusHandler(elem) {
        return function() {

            // Only hide the placeholder value if the (default) hide-on-focus
            // behaviour is enabled.
            if (
                hideOnInput &&
                elem.value === elem.getAttribute(ATTR_CURRENT_VAL) &&
                elem.getAttribute(ATTR_ACTIVE) === 'true'
            ) {

                // Move the caret to the start of the input (this mimics the behaviour
                // of all browsers that do not hide the placeholder on focus).
                moveCaret(elem, 0);
            } else {

                // Remove the placeholder.
                hidePlaceholder(elem);
            }
        };
    }

    // Returns a function that is used as a blur event handler.
    function makeBlurHandler(elem) {
        return function() {
            showPlaceholder(elem);
        };
    }

    // Returns a function that is used as a submit event handler on form elements
    // that have children affected by this polyfill.
    function makeSubmitHandler(form) {
        return function() {

            // Turn off placeholders on all appropriate descendant elements.
            disablePlaceholders(form);
        };
    }

    // Functions that are used as a event handlers when the hide-on-input
    // behaviour has been activated - very basic implementation of the 'input'
    // event.
    function makeKeydownHandler(elem) {
        return function(e) {
            keydownVal = elem.value;

            // Prevent the use of the arrow keys (try to keep the cursor before the
            // placeholder).
            if (
                elem.getAttribute(ATTR_ACTIVE) === 'true' &&
                keydownVal === elem.getAttribute(ATTR_CURRENT_VAL) &&
                inArray(badKeys, e.keyCode)
            ) {
                if (e.preventDefault) {
                    e.preventDefault();
                }
                return false;
            }
        };
    }

    function makeKeyupHandler(elem) {
        return function() {
            hidePlaceholder(elem, keydownVal);

            // If the element is now empty we need to show the placeholder
            if (elem.value === '') {
                elem.blur();
                moveCaret(elem, 0);
            }
        };
    }

    function makeClickHandler(elem) {
        return function() {
            if (
                elem === safeActiveElement() &&
                elem.value === elem.getAttribute(ATTR_CURRENT_VAL) &&
                elem.getAttribute(ATTR_ACTIVE) === 'true'
            ) {
                moveCaret(elem, 0);
            }
        };
    }

    // Bind event handlers to an element that we need to affect with the
    // polyfill.
    function newElement(elem) {

        // If the element is part of a form, make sure the placeholder string is
        // not submitted as a value.
        var form = elem.form;
        if (form && typeof form === 'string') {

            // Get the real form.
            form = document.getElementById(form);

            // Set a flag on the form so we know it's been handled (forms can contain
            // multiple inputs).
            if (!form.getAttribute(ATTR_FORM_HANDLED)) {
                addEventListener(form, 'submit', makeSubmitHandler(form));
                form.setAttribute(ATTR_FORM_HANDLED, 'true');
            }
        }

        // Bind event handlers to the element so we can hide/show the placeholder
        // as appropriate.
        addEventListener(elem, 'focus', makeFocusHandler(elem));
        addEventListener(elem, 'blur', makeBlurHandler(elem));

        // If the placeholder should hide on input rather than on focus we need
        // additional event handlers
        if (hideOnInput) {
            addEventListener(elem, 'keydown', makeKeydownHandler(elem));
            addEventListener(elem, 'keyup', makeKeyupHandler(elem));
            addEventListener(elem, 'click', makeClickHandler(elem));
        }

        // Remember that we've bound event handlers to this element.
        elem.setAttribute(ATTR_EVENTS_BOUND, 'true');
        elem.setAttribute(ATTR_CURRENT_VAL, placeholder);

        // If the element doesn't have a value and is not focussed, set it to the
        // placeholder string.
        if (hideOnInput || elem !== safeActiveElement()) {
            showPlaceholder(elem);
        }
    }

}(this));
